package testing;

import java.io.BufferedOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Arrays;

import data.DataInfo;
import data.DataLookup;

public class SVDFeaturePredictor implements NetflixPredictor {
	/*
	 * This is an implimentation of a psuedo SVD algorithm. Originally written
	 * by Timely Development and was then ported to the Netflix framework.
	 * 
	 * Robert Layton then ported it to jNetflix: (c) 2008
	 */
	private final static int NUM_FEATURES = 64;

	private final static int MIN_EPOCHS = 120;

	// private final static int MAX_EPOCHS = 240;

	private final static double LEARNING_RATE = 0.001;

	private final static double K_m = 0.015;

	private final static double K_u = 0.015;

	private final static double INIT_VALUE = 0.1;

	private final static double MIN_IMPROVEMENT = 0.0001;

	private final static int MAX_RATINGS = 100480508;

	private double[][] UserFeatures = new double[NUM_FEATURES][DataInfo.NUM_USERS];

	private double[][] MovieFeatures = new double[NUM_FEATURES][DataInfo.NUM_MOVIES];

	private double[] cache = new double[MAX_RATINGS + 1];

	private static DataLookup dl = DataLookup.getInstance();

	public SVDFeaturePredictor() {
		Arrays.fill(cache, 0.0f);
		for (int i = 0; i < NUM_FEATURES; i++) {
			for (int j = 0; j < DataInfo.NUM_USERS; j++) {
				UserFeatures[i][j] = initValue();
			}
			for (int j = 0; j < DataInfo.NUM_MOVIES; j++) {
				MovieFeatures[i][j] = initValue();
			}
		}

		calculateFeatures();
		saveMatrices("completed");
	}
	private void saveMatrices(String filename) {
		System.out.print("Blitting matrices to a file...");
		filename = DataInfo.binaryFolder + "/svd." + filename + ".matrix";
		BufferedOutputStream out = null;
		ObjectOutputStream oos = null;
		try {
			out = new BufferedOutputStream(new FileOutputStream(filename));
			oos = new ObjectOutputStream(out);
			// Write the arrays to the file
			oos.writeObject(UserFeatures);
			System.gc();
			oos.writeObject(MovieFeatures);
			oos.close();
			out.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		System.out.println("done");
	}

	private double globalAverage = -1;

	private double initValue() {
		if (globalAverage < 1) {
			// Calc the global average
			globalAverage = 0;
			for (short movieID = 1; movieID <= DataInfo.NUM_MOVIES; movieID++) {
				globalAverage += dl.getMovieAverage(movieID);
			}
			globalAverage /= DataInfo.NUM_MOVIES;
		}
		// See Ma (2008), pages 4, 5
		double r = INIT_VALUE;
		return Math.sqrt((globalAverage - 1) / NUM_FEATURES)
				+ ((2 * r * Math.random()) - r);
	}

	private void calculateFeatures() {
		double rmse_last = 2.0, rmse = 2.0;
		for (int f = 0; f < 5 && f < NUM_FEATURES; f++) {
			System.out.println("Calculating feature: " + f);
			// look until min_epoch, or improvement is better then last epoch
			for (int epoch = 0; (epoch < MIN_EPOCHS)
					|| (rmse <= rmse_last - MIN_IMPROVEMENT); epoch++) {
				double squaredError = 0.0;
				int cacheID = 0;
				rmse_last = rmse;
				for (int userIndex = 0; userIndex < DataInfo.NUM_USERS; userIndex++) {
					short[] ratedMovies = dl.getUsersMovies(userIndex);
					byte[] ratings = dl.getUsersRatings(userIndex);
					for (int mi = 0; mi < ratedMovies.length; mi++) {
						double p = getPrediction(userIndex,
								ratedMovies[mi] - 1, f, cache[cacheID], true);

						double err = 1.0 * ratings[mi] - p;
						squaredError += err * err;

						// Cross-train the features
						double uf = UserFeatures[f][userIndex];
						double mf = MovieFeatures[f][ratedMovies[mi] - 1];
						UserFeatures[f][userIndex] += LEARNING_RATE
								* (err * mf - K_u * uf);
						MovieFeatures[f][ratedMovies[mi] - 1] += LEARNING_RATE
								* (err * uf - K_m * mf);
						cacheID++;
					}
				}
				rmse = Math.sqrt(squaredError / MAX_RATINGS);
				System.out.printf("\t<set x='%d' y='%f' e='%d'/>\n", epoch * f
						+ epoch, rmse, epoch);
			}

			// set cache
			int cacheID = 0;
			for (int userIndex = 0; userIndex < DataInfo.NUM_USERS; userIndex++) {
				short[] ratedMovies = dl.getUsersMovies(userIndex);
				for (int mi = 0; mi < ratedMovies.length; mi++) {
					cache[cacheID] = getPrediction(userIndex,
							ratedMovies[mi] - 1, f, cache[cacheID], false);
					cacheID++;
				}
			}
		}
	}

	private double getPrediction(final int userIndex, final int movieIndex,
			final int feature, final double cache, final boolean trailing) {
		// Get cached value for old features or default to an average
		double sum = (cache > 0.0) ? cache : 1.0; // m_aMovies[movieId].PseudoAvg;

		// Add contribution of current feature
		sum += MovieFeatures[feature][movieIndex]
				* UserFeatures[feature][userIndex];

		if (trailing) {
			sum += (NUM_FEATURES - feature - 1) * (INIT_VALUE * INIT_VALUE);
		}

		// clip to bounds
		if (sum < 1.0)
			sum = 1.0;
		else if (sum > 5.0)
			sum = 5.0;

		return sum;
	}

	public double predictRating(int userID, short movieID) {
		double sum = 1; // m_aMovies[movieId].PseudoAvg;

		int userIndex = DataInfo.getUserIndex(userID);

		for (int feature = 0; feature < NUM_FEATURES; feature++) {
			sum += MovieFeatures[feature][movieID - 1]
					* UserFeatures[feature][userIndex];
		}
		if (sum < 1.0)
			sum = 1.0;
		else if (sum > 5.0)
			sum = 5.0;
		return sum;
	}

	public double predictRating(int userID, short movieID, String date) {
		return predictRating(userID, movieID);
	}

	public static void main(String args[]) {
		DataLookup.getInstance();

		boolean userIndexed = false;
		boolean showErrorUpdates = false;

		System.out.println("Normalized predictor===========");
		System.out.println("Robert Layton. (c) 2008");
		System.out
				.println("May not be used without permission from the author");
		SVDFeaturePredictor tp = new SVDFeaturePredictor();
		System.out.println("Calculating error on probe dataset");

		double error = PredictionTester.getProbeError(tp, userIndexed,
				showErrorUpdates);

		System.out.println("Error on probe dataset is: " + error);
		String outputFilename = "/home/bob/qual01.txt";
		System.out.println("Creating Qualification file at " + outputFilename);
		PredictionTester.createQualifyingSubmission(tp, outputFilename);

	}
}
